import { NesUtil } from "..";
import { NesPlayerHankInfo } from "../../NesPlayer";
import { Inst, OpData } from "./OpData";

/** 寻址模式 */
export enum AddrMode  {
    /** 使用操作码后给出的地址，但没有高字节 */
    ZeroPage,
    /** 相对模式 */
    RelativeMode,
    /** 指令中隐含地址 */
    Ignore,
    /** 使用操作码后面的两个字节作为地址 */
    AbsoluteMode,
    /** 地址在累加器寄存器中 */
    AccumulatorMode,
    /** 该值在操作码之后给出 */
    ImmediateMode,
    /** 使用操作码后给出的地址，然后向其添加X寄存器以获得最终地址 */
    ZeroPageIndexedModeX,
    /** 使用操作码后给出的地址，然后向其添加Y寄存器以获得最终地址 */
    ZeroPageIndexedModeY,
    /** 与零页索引相同，但使用了高字节 */
    AbsoluteIndexedModeX,
    /** 与零页索引相同，但使用了高字节 */
    AbsoluteIndexedModeY,
    /** 查找从给定位置开始的16位地址加上当前的X寄存器。该值是该地址的内容 */
    PreIndexedIndirectMode,
    /** 查找包含在给定位置中的16位地址(以及后面的地址)。向该地址添加Y寄存器的内容。获取存储在该地址的值 */
    PostIndexedIndirectMode,
    /** 查找包含在给定位置的16位地址 */
    IndirectAbsoluteMode,
}

export enum IRQType {
    NORMAL,
    NMI,
    RESET,
}

export class CPU {
    private nes: NesUtil;
    private REG_ACC: number;
    private REG_X: number;
    private REG_Y: number;
    private REG_SP: number;
    private REG_PC: number;
    private REG_PC_NEW: number;
    private REG_STATUS: number;
    private F_CARRY: number;
    private F_DECIMAL: number;
    private F_INTERRUPT: number;
    private F_INTERRUPT_NEW: number;
    private F_OVERFLOW: number;
    private F_SIGN: number;
    private F_ZERO: number;
    private F_NOTUSED: number;
    private F_NOTUSED_NEW: number;
    private F_BRK: number;
    private F_BRK_NEW: number;
    private opdata: Uint32Array;
    private crash: boolean;
    private irqRequested: boolean;
    private irqType: IRQType;

    public mem: Uint8Array;
    public cyclesToHalt: number;
    public hack?: NesPlayerHankInfo[];

    constructor(nes: NesUtil) {
        this.nes = nes;
        // Keep Chrome happy
        this.reset();
    }

    reset() {
        // Main memory
        this.mem = new Uint8Array(0x10000);

        for (let i = 0; i < 0x2000; i++) {
            this.mem[i] = 0xFF;
        }
        for (let p = 0; p < 4; p++) {
            const i = p * 0x800;
            this.mem[i + 0x008] = 0xF7;
            this.mem[i + 0x009] = 0xEF;
            this.mem[i + 0x00A] = 0xDF;
            this.mem[i + 0x00F] = 0xBF;
        }
        for (let i = 0x2001; i < this.mem.length; i++) {
            this.mem[i] = 0;
        }

        // CPU Registers:
        this.REG_ACC = 0;
        this.REG_X = 0;
        this.REG_Y = 0;
        // Reset Stack pointer:
        this.REG_SP = 0x01FF;
        // Reset Program counter:
        this.REG_PC = 0x8000 - 1;
        this.REG_PC_NEW = 0x8000 - 1;
        // Reset Status register:
        this.REG_STATUS = 0x28;

        this.setStatus(0x28);

        // Set flags:
        this.F_CARRY = 0;
        this.F_DECIMAL = 0;
        this.F_INTERRUPT = 1;
        this.F_INTERRUPT_NEW = 1;
        this.F_OVERFLOW = 0;
        this.F_SIGN = 0;
        this.F_ZERO = 1;

        this.F_NOTUSED = 1;
        this.F_NOTUSED_NEW = 1;
        this.F_BRK = 1;
        this.F_BRK_NEW = 1;

        this.opdata = new OpData().opdata;
        this.cyclesToHalt = 0;

        // Reset crash flag:
        this.crash = false;

        // Interrupt notification:
        this.irqRequested = false;
        this.irqType = null;
    }

    // Emulates a single CPU instruction, returns the number of cycles
    emulate() {
        let temp: number;
        let add: number;

        // Check interrupts:
        if (this.irqRequested) {
            temp =
                (this.F_CARRY) |
                ((this.F_ZERO === 0 ? 1 : 0) << 1) |
                (this.F_INTERRUPT << 2) |
                (this.F_DECIMAL << 3) |
                (this.F_BRK << 4) |
                (this.F_NOTUSED << 5) |
                (this.F_OVERFLOW << 6) |
                (this.F_SIGN << 7);

            this.REG_PC_NEW = this.REG_PC;
            this.F_INTERRUPT_NEW = this.F_INTERRUPT;
            switch (this.irqType) {
                case 0: {
                    // Normal IRQ: 正常的硬中断请求优先级别
                    if (this.F_INTERRUPT != 0) {
                        ////System.out.println("Interrupt was masked.");
                        break;
                    }
                    this.doIrq(temp);
                    ////System.out.println("Did normal IRQ. I="+this.F_INTERRUPT);
                    break;
                } case 1: {
                    // NMI: 敝中断
                    this.doNonMaskableInterrupt(temp);
                    break;

                } case 2: {
                    // Reset: 重置
                    this.doResetInterrupt();
                    break;
                }
            }

            this.REG_PC = this.REG_PC_NEW;
            this.F_INTERRUPT = this.F_INTERRUPT_NEW;
            this.F_BRK = this.F_BRK_NEW;
            this.irqRequested = false;
        }

        const opinf = this.opdata[this.nes.mmap.load(this.REG_PC + 1)];
        let cycleCount = (opinf >> 24);
        let cycleAdd = 0;

        /** Find address mode: 寻址模式 */
        const addrMode: AddrMode = (opinf >> 8) & 0xFF;

        /** Increment PC by number of op bytes: 按op字节数增加PC */
        const opaddr = this.REG_PC;
        this.REG_PC += ((opinf >> 16) & 0xFF);

        let addr = 0;
        switch (addrMode) {
            case AddrMode.ZeroPage: {
                // Zero Page mode. Use the address given after the opcode,
                // but without high byte.
                addr = this.load(opaddr + 2);
                break;

            } case AddrMode.RelativeMode: {
                // Relative mode.
                addr = this.load(opaddr + 2);
                if (addr < 0x80) {
                    addr += this.REG_PC;
                } else {
                    addr += this.REG_PC - 256;
                }
                break;
            } case AddrMode.Ignore: {
                // Ignore. Address is implied in instruction.
                break;
            } case AddrMode.AbsoluteMode: {
                // Absolute mode. Use the two bytes following the opcode as
                // an address.
                addr = this.load16bit(opaddr + 2);
                break;
            } case AddrMode.AccumulatorMode: {
                // Accumulator mode. The address is in the accumulator
                // register.
                addr = this.REG_ACC;
                break;
            } case AddrMode.ImmediateMode: {
                // Immediate mode. The value is given after the opcode.
                addr = this.REG_PC;
                break;
            } case AddrMode.ZeroPageIndexedModeX: {
                // Zero Page Indexed mode, X as index. Use the address given
                // after the opcode, then add the
                // X register to it to get the final address.
                addr = (this.load(opaddr + 2) + this.REG_X) & 0xFF;
                break;
            } case AddrMode.ZeroPageIndexedModeY: {
                // Zero Page Indexed mode, Y as index. Use the address given
                // after the opcode, then add the
                // Y register to it to get the final address.
                addr = (this.load(opaddr + 2) + this.REG_Y) & 0xFF;
                break;
            } case AddrMode.AbsoluteIndexedModeX: {
                // Absolute Indexed Mode, X as index. Same as zero page
                // indexed, but with the high byte.
                addr = this.load16bit(opaddr + 2);
                if ((addr & 0xFF00) != ((addr + this.REG_X) & 0xFF00)) {
                    cycleAdd = 1;
                }
                addr += this.REG_X;
                break;
            } case AddrMode.AbsoluteIndexedModeY: {
                // Absolute Indexed Mode, Y as index. Same as zero page
                // indexed, but with the high byte.
                addr = this.load16bit(opaddr + 2);
                if ((addr & 0xFF00) != ((addr + this.REG_Y) & 0xFF00)) {
                    cycleAdd = 1;
                }
                addr += this.REG_Y;
                break;
            } case AddrMode.PreIndexedIndirectMode: {
                // Pre-indexed Indirect mode. Find the 16-bit address
                // starting at the given location plus
                // the current X register. The value is the contents of that
                // address.
                addr = this.load(opaddr + 2);
                if ((addr & 0xFF00) != ((addr + this.REG_X) & 0xFF00)) {
                    cycleAdd = 1;
                }
                addr += this.REG_X;
                addr &= 0xFF;
                addr = this.load16bit(addr);
                break;
            } case AddrMode.PostIndexedIndirectMode: {
                // Post-indexed Indirect mode. Find the 16-bit address
                // contained in the given location
                // (and the one following). Add to that address the contents
                // of the Y register. Fetch the value
                // stored at that adress.
                addr = this.load16bit(this.load(opaddr + 2));
                if ((addr & 0xFF00) != ((addr + this.REG_Y) & 0xFF00)) {
                    cycleAdd = 1;
                }
                addr += this.REG_Y;
                break;
            } case AddrMode.IndirectAbsoluteMode: {
                // Indirect Absolute mode. Find the 16-bit address contained
                // at the given location.
                addr = this.load16bit(opaddr + 2);// Find op
                if (addr < 0x1FFF) {
                    addr = this.mem[addr] + (this.mem[(addr & 0xFF00) | (((addr & 0xFF) + 1) & 0xFF)] << 8);// Read from address given in op
                }
                else {
                    addr = this.nes.mmap.load(addr) + (this.nes.mmap.load((addr & 0xFF00) | (((addr & 0xFF) + 1) & 0xFF)) << 8);
                }
                break;

            }

        }
        // Wrap around for addresses above 0xFFFF: 包裹上面的地址
        addr &= 0xFFFF;

        // ----------------------------------------------------------------------------------------------------
        // Decode & execute instruction:
        // ----------------------------------------------------------------------------------------------------

        // This should be compiled to a jump table.
        // 应该将其编译为一个跳转表。
        switch (opinf & 0xFF) {
            case Inst.ADC: {
                // *******
                // * ADC *
                // *******

                // Add with carry.
                temp = this.REG_ACC + this.load(addr) + this.F_CARRY;
                this.F_OVERFLOW = ((!(((this.REG_ACC ^ this.load(addr)) & 0x80) != 0) && (((this.REG_ACC ^ temp) & 0x80)) != 0) ? 1 : 0);
                this.F_CARRY = (temp > 255 ? 1 : 0);
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp & 0xFF;
                this.REG_ACC = (temp & 255);
                cycleCount += cycleAdd;
                break;

            } case Inst.AND: {
                // *******
                // * AND *
                // *******

                // AND memory with accumulator.
                this.REG_ACC = this.REG_ACC & this.load(addr);
                this.F_SIGN = (this.REG_ACC >> 7) & 1;
                this.F_ZERO = this.REG_ACC;
                //this.REG_ACC = temp;
                if (addrMode != 11) cycleCount += cycleAdd; // PostIdxInd = 11
                break;
            } case Inst.ASL: {
                // *******
                // * ASL *
                // *******

                // Shift left one bit
                if (addrMode == 4) { // ADDR_ACC = 4

                    this.F_CARRY = (this.REG_ACC >> 7) & 1;
                    this.REG_ACC = (this.REG_ACC << 1) & 255;
                    this.F_SIGN = (this.REG_ACC >> 7) & 1;
                    this.F_ZERO = this.REG_ACC;

                } else {

                    temp = this.load(addr);
                    this.F_CARRY = (temp >> 7) & 1;
                    temp = (temp << 1) & 255;
                    this.F_SIGN = (temp >> 7) & 1;
                    this.F_ZERO = temp;
                    this.write(addr, temp);

                }
                break;

            } case Inst.BCC: {

                // *******
                // * BCC *
                // *******

                // Branch on carry clear
                if (this.F_CARRY == 0) {
                    cycleCount += ((opaddr & 0xFF00) != (addr & 0xFF00) ? 2 : 1);
                    this.REG_PC = addr;
                }
                break;

            } case Inst.BCS: {

                // *******
                // * BCS *
                // *******

                // Branch on carry set
                if (this.F_CARRY == 1) {
                    cycleCount += ((opaddr & 0xFF00) != (addr & 0xFF00) ? 2 : 1);
                    this.REG_PC = addr;
                }
                break;

            } case Inst.BEQ: {

                // *******
                // * BEQ *
                // *******

                // Branch on zero
                if (this.F_ZERO == 0) {
                    cycleCount += ((opaddr & 0xFF00) != (addr & 0xFF00) ? 2 : 1);
                    this.REG_PC = addr;
                }
                break;

            } case Inst.BIT: {

                // *******
                // * BIT *
                // *******

                temp = this.load(addr);
                this.F_SIGN = (temp >> 7) & 1;
                this.F_OVERFLOW = (temp >> 6) & 1;
                temp &= this.REG_ACC;
                this.F_ZERO = temp;
                break;

            } case Inst.BMI: {

                // *******
                // * BMI *
                // *******

                // Branch on negative result
                if (this.F_SIGN == 1) {
                    cycleCount++;
                    this.REG_PC = addr;
                }
                break;

            } case Inst.BNE: {

                // *******
                // * BNE *
                // *******

                // Branch on not zero
                if (this.F_ZERO != 0) {
                    cycleCount += ((opaddr & 0xFF00) != (addr & 0xFF00) ? 2 : 1);
                    this.REG_PC = addr;
                }
                break;

            } case Inst.BPL: {

                // *******
                // * BPL *
                // *******

                // Branch on positive result
                if (this.F_SIGN == 0) {
                    cycleCount += ((opaddr & 0xFF00) != (addr & 0xFF00) ? 2 : 1);
                    this.REG_PC = addr;
                }
                break;

            } case Inst.BRK: {

                // *******
                // * BRK *
                // *******

                this.REG_PC += 2;
                this.push((this.REG_PC >> 8) & 255);
                this.push(this.REG_PC & 255);
                this.F_BRK = 1;

                this.push(
                    (this.F_CARRY) |
                    ((this.F_ZERO == 0 ? 1 : 0) << 1) |
                    (this.F_INTERRUPT << 2) |
                    (this.F_DECIMAL << 3) |
                    (this.F_BRK << 4) |
                    (this.F_NOTUSED << 5) |
                    (this.F_OVERFLOW << 6) |
                    (this.F_SIGN << 7)
                );

                this.F_INTERRUPT = 1;
                //this.REG_PC = load(0xFFFE) | (load(0xFFFF) << 8);
                this.REG_PC = this.load16bit(0xFFFE);
                this.REG_PC--;
                break;

            } case Inst.BVC: {

                // *******
                // * BVC *
                // *******

                // Branch on overflow clear
                if (this.F_OVERFLOW == 0) {
                    cycleCount += ((opaddr & 0xFF00) != (addr & 0xFF00) ? 2 : 1);
                    this.REG_PC = addr;
                }
                break;

            } case Inst.BVS: {

                // *******
                // * BVS *
                // *******

                // Branch on overflow set
                if (this.F_OVERFLOW == 1) {
                    cycleCount += ((opaddr & 0xFF00) != (addr & 0xFF00) ? 2 : 1);
                    this.REG_PC = addr;
                }
                break;

            } case Inst.CLC: {

                // *******
                // * CLC *
                // *******

                // Clear carry flag
                this.F_CARRY = 0;
                break;

            } case Inst.CLD: {

                // *******
                // * CLD *
                // *******

                // Clear decimal flag
                this.F_DECIMAL = 0;
                break;

            } case Inst.CLI: {

                // *******
                // * CLI *
                // *******

                // Clear interrupt flag
                this.F_INTERRUPT = 0;
                break;

            } case Inst.CLV: {

                // *******
                // * CLV *
                // *******

                // Clear overflow flag
                this.F_OVERFLOW = 0;
                break;

            } case Inst.CMP: {

                // *******
                // * CMP *
                // *******

                // Compare memory and accumulator:
                temp = this.REG_ACC - this.load(addr);
                this.F_CARRY = (temp >= 0 ? 1 : 0);
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp & 0xFF;
                cycleCount += cycleAdd;
                break;

            } case Inst.CPX: {

                // *******
                // * CPX *
                // *******

                // Compare memory and index X:
                temp = this.REG_X - this.load(addr);
                this.F_CARRY = (temp >= 0 ? 1 : 0);
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp & 0xFF;
                break;

            } case Inst.CPY: {

                // *******
                // * CPY *
                // *******

                // Compare memory and index Y:
                temp = this.REG_Y - this.load(addr);
                this.F_CARRY = (temp >= 0 ? 1 : 0);
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp & 0xFF;
                break;

            } case Inst.DEC: {

                // *******
                // * DEC *
                // *******

                // Decrement memory by one:
                temp = (this.load(addr) - 1) & 0xFF;
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp;
                this.write(addr, temp);
                break;

            } case Inst.DEX: {

                // *******
                // * DEX *
                // *******

                // Decrement index X by one:
                this.REG_X = (this.REG_X - 1) & 0xFF;
                this.F_SIGN = (this.REG_X >> 7) & 1;
                this.F_ZERO = this.REG_X;
                break;

            } case Inst.DEY: {

                // *******
                // * DEY *
                // *******

                // Decrement index Y by one:
                this.REG_Y = (this.REG_Y - 1) & 0xFF;
                this.F_SIGN = (this.REG_Y >> 7) & 1;
                this.F_ZERO = this.REG_Y;
                break;

            } case Inst.EOR: {

                // *******
                // * EOR *
                // *******

                // XOR Memory with accumulator, store in accumulator:
                this.REG_ACC = (this.load(addr) ^ this.REG_ACC) & 0xFF;
                this.F_SIGN = (this.REG_ACC >> 7) & 1;
                this.F_ZERO = this.REG_ACC;
                cycleCount += cycleAdd;
                break;

            } case Inst.INC: {

                // *******
                // * INC *
                // *******

                // Increment memory by one:
                temp = (this.load(addr) + 1) & 0xFF;
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp;
                this.write(addr, temp & 0xFF);
                break;

            } case Inst.INX: {

                // *******
                // * INX *
                // *******

                // Increment index X by one:
                this.REG_X = (this.REG_X + 1) & 0xFF;
                this.F_SIGN = (this.REG_X >> 7) & 1;
                this.F_ZERO = this.REG_X;
                break;

            } case Inst.INY: {

                // *******
                // * INY *
                // *******

                // Increment index Y by one:
                this.REG_Y++;
                this.REG_Y &= 0xFF;
                this.F_SIGN = (this.REG_Y >> 7) & 1;
                this.F_ZERO = this.REG_Y;
                break;

            } case Inst.JMP: {

                // *******
                // * JMP *
                // *******

                // Jump to new location:
                this.REG_PC = addr - 1;
                break;

            } case Inst.JSR: {

                // *******
                // * JSR *
                // *******

                // Jump to new location, saving return address.
                // Push return address on stack:
                this.push((this.REG_PC >> 8) & 255);
                this.push(this.REG_PC & 255);
                this.REG_PC = addr - 1;
                break;

            } case Inst.LDA: {

                // *******
                // * LDA *
                // *******

                // Load accumulator with memory:
                this.REG_ACC = this.load(addr);
                this.F_SIGN = (this.REG_ACC >> 7) & 1;
                this.F_ZERO = this.REG_ACC;
                cycleCount += cycleAdd;
                break;

            } case Inst.LDX: {

                // *******
                // * LDX *
                // *******

                // Load index X with memory:
                this.REG_X = this.load(addr);
                this.F_SIGN = (this.REG_X >> 7) & 1;
                this.F_ZERO = this.REG_X;
                cycleCount += cycleAdd;
                break;

            } case Inst.LDY: {

                // *******
                // * LDY *
                // *******

                // Load index Y with memory:
                this.REG_Y = this.load(addr);
                this.F_SIGN = (this.REG_Y >> 7) & 1;
                this.F_ZERO = this.REG_Y;
                cycleCount += cycleAdd;
                break;

            } case Inst.LSR: {

                // *******
                // * LSR *
                // *******

                // Shift right one bit:
                if (addrMode == 4) { // ADDR_ACC

                    temp = (this.REG_ACC & 0xFF);
                    this.F_CARRY = temp & 1;
                    temp >>= 1;
                    this.REG_ACC = temp;

                } else {

                    temp = this.load(addr) & 0xFF;
                    this.F_CARRY = temp & 1;
                    temp >>= 1;
                    this.write(addr, temp);

                }
                this.F_SIGN = 0;
                this.F_ZERO = temp;
                break;

            } case Inst.NOP: {

                // *******
                // * NOP *
                // *******

                // No OPeration.
                // Ignore.
                break;

            } case Inst.ORA: {

                // *******
                // * ORA *
                // *******

                // OR memory with accumulator, store in accumulator.
                temp = (this.load(addr) | this.REG_ACC) & 255;
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp;
                this.REG_ACC = temp;
                if (addrMode != 11) cycleCount += cycleAdd; // PostIdxInd = 11
                break;

            } case Inst.PHA: {

                // *******
                // * PHA *
                // *******

                // Push accumulator on stack
                this.push(this.REG_ACC);
                break;

            } case Inst.PHP: {

                // *******
                // * PHP *
                // *******

                // Push processor status on stack
                this.F_BRK = 1;
                this.push(
                    (this.F_CARRY) |
                    ((this.F_ZERO == 0 ? 1 : 0) << 1) |
                    (this.F_INTERRUPT << 2) |
                    (this.F_DECIMAL << 3) |
                    (this.F_BRK << 4) |
                    (this.F_NOTUSED << 5) |
                    (this.F_OVERFLOW << 6) |
                    (this.F_SIGN << 7)
                );
                break;

            } case Inst.PLA: {

                // *******
                // * PLA *
                // *******

                // Pull accumulator from stack
                this.REG_ACC = this.pull();
                this.F_SIGN = (this.REG_ACC >> 7) & 1;
                this.F_ZERO = this.REG_ACC;
                break;

            } case Inst.PLP: {

                // *******
                // * PLP *
                // *******

                // Pull processor status from stack
                temp = this.pull();
                this.F_CARRY = (temp) & 1;
                this.F_ZERO = (((temp >> 1) & 1) == 1) ? 0 : 1;
                this.F_INTERRUPT = (temp >> 2) & 1;
                this.F_DECIMAL = (temp >> 3) & 1;
                this.F_BRK = (temp >> 4) & 1;
                this.F_NOTUSED = (temp >> 5) & 1;
                this.F_OVERFLOW = (temp >> 6) & 1;
                this.F_SIGN = (temp >> 7) & 1;

                this.F_NOTUSED = 1;
                break;

            } case Inst.ROL: {

                // *******
                // * ROL *
                // *******

                // Rotate one bit left
                if (addrMode == 4) { // ADDR_ACC = 4

                    temp = this.REG_ACC;
                    add = this.F_CARRY;
                    this.F_CARRY = (temp >> 7) & 1;
                    temp = ((temp << 1) & 0xFF) + add;
                    this.REG_ACC = temp;

                } else {

                    temp = this.load(addr);
                    add = this.F_CARRY;
                    this.F_CARRY = (temp >> 7) & 1;
                    temp = ((temp << 1) & 0xFF) + add;
                    this.write(addr, temp);

                }
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp;
                break;

            } case Inst.ROR: {

                // *******
                // * ROR *
                // *******

                // Rotate one bit right
                if (addrMode == 4) { // ADDR_ACC = 4

                    add = this.F_CARRY << 7;
                    this.F_CARRY = this.REG_ACC & 1;
                    temp = (this.REG_ACC >> 1) + add;
                    this.REG_ACC = temp;

                } else {

                    temp = this.load(addr);
                    add = this.F_CARRY << 7;
                    this.F_CARRY = temp & 1;
                    temp = (temp >> 1) + add;
                    this.write(addr, temp);

                }
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp;
                break;

            } case Inst.RTI: {

                // *******
                // * RTI *
                // *******

                // Return from interrupt. Pull status and PC from stack.

                temp = this.pull();
                this.F_CARRY = (temp) & 1;
                this.F_ZERO = ((temp >> 1) & 1) == 0 ? 1 : 0;
                this.F_INTERRUPT = (temp >> 2) & 1;
                this.F_DECIMAL = (temp >> 3) & 1;
                this.F_BRK = (temp >> 4) & 1;
                this.F_NOTUSED = (temp >> 5) & 1;
                this.F_OVERFLOW = (temp >> 6) & 1;
                this.F_SIGN = (temp >> 7) & 1;

                this.REG_PC = this.pull();
                this.REG_PC += (this.pull() << 8);
                if (this.REG_PC == 0xFFFF) {
                    return;
                }
                this.REG_PC--;
                this.F_NOTUSED = 1;
                break;

            } case Inst.RTS: {

                // *******
                // * RTS *
                // *******

                // Return from subroutine. Pull PC from stack.

                this.REG_PC = this.pull();
                this.REG_PC += (this.pull() << 8);

                if (this.REG_PC == 0xFFFF) {
                    return; // return from NSF play routine:
                }
                break;

            } case Inst.SBC: {

                // *******
                // * SBC *
                // *******

                temp = this.REG_ACC - this.load(addr) - (1 - this.F_CARRY);
                this.F_SIGN = (temp >> 7) & 1;
                this.F_ZERO = temp & 0xFF;
                this.F_OVERFLOW = ((((this.REG_ACC ^ temp) & 0x80) != 0 && ((this.REG_ACC ^ this.load(addr)) & 0x80) != 0) ? 1 : 0);
                this.F_CARRY = (temp < 0 ? 0 : 1);
                this.REG_ACC = (temp & 0xFF);
                if (addrMode != 11) cycleCount += cycleAdd; // PostIdxInd = 11
                break;

            } case Inst.SEC: {

                // *******
                // * SEC *
                // *******

                // Set carry flag
                this.F_CARRY = 1;
                break;

            } case Inst.SED: {

                // *******
                // * SED *
                // *******

                // Set decimal mode
                this.F_DECIMAL = 1;
                break;

            } case Inst.SEI: {

                // *******
                // * SEI *
                // *******

                // Set interrupt disable status
                this.F_INTERRUPT = 1;
                break;

            } case Inst.STA: {

                // *******
                // * STA *
                // *******

                // Store accumulator in memory
                this.write(addr, this.REG_ACC);
                break;

            } case Inst.STX: {

                // *******
                // * STX *
                // *******

                // Store index X in memory
                this.write(addr, this.REG_X);
                break;

            } case Inst.STY: {

                // *******
                // * STY *
                // *******

                // Store index Y in memory:
                this.write(addr, this.REG_Y);
                break;

            } case Inst.TAX: {

                // *******
                // * TAX *
                // *******

                // Transfer accumulator to index X:
                this.REG_X = this.REG_ACC;
                this.F_SIGN = (this.REG_ACC >> 7) & 1;
                this.F_ZERO = this.REG_ACC;
                break;

            } case Inst.TAY: {

                // *******
                // * TAY *
                // *******

                // Transfer accumulator to index Y:
                this.REG_Y = this.REG_ACC;
                this.F_SIGN = (this.REG_ACC >> 7) & 1;
                this.F_ZERO = this.REG_ACC;
                break;

            } case Inst.TSX: {

                // *******
                // * TSX *
                // *******

                // Transfer stack pointer to index X:
                this.REG_X = (this.REG_SP - 0x0100);
                this.F_SIGN = (this.REG_SP >> 7) & 1;
                this.F_ZERO = this.REG_X;
                break;

            } case Inst.TXA: {

                // *******
                // * TXA *
                // *******

                // Transfer index X to accumulator:
                this.REG_ACC = this.REG_X;
                this.F_SIGN = (this.REG_X >> 7) & 1;
                this.F_ZERO = this.REG_X;
                break;

            } case Inst.TXS: {

                // *******
                // * TXS *
                // *******

                // Transfer index X to stack pointer:
                this.REG_SP = (this.REG_X + 0x0100);
                this.stackWrap();
                break;

            } case Inst.TYA: {

                // *******
                // * TYA *
                // *******

                // Transfer index Y to accumulator:
                this.REG_ACC = this.REG_Y;
                this.F_SIGN = (this.REG_Y >> 7) & 1;
                this.F_ZERO = this.REG_Y;
                break;

            } default: {

                // *******
                // * ??? *
                // *******

                this.nes.stop();
                this.nes.opts.updateStatus("Game crashed, invalid opcode at address $" + opaddr.toString(16));
                throw new Error("Game crashed, invalid opcode at address $" + opaddr.toString(16));
                break;

            }

        }// end of switch

        return cycleCount;

    }

    load(addr: number) {
        if (addr < 0x2000) {
            return this.mem[addr & 0x7FF];
        }
        else {
            return this.nes.mmap.load(addr);
        }
    }

    load16bit(addr: number) {
        if (addr < 0x1FFF) {
            return this.mem[addr & 0x7FF]
                | (this.mem[(addr + 1) & 0x7FF] << 8);
        }
        else {
            return this.nes.mmap.load(addr) | (this.nes.mmap.load(addr + 1) << 8);
        }
    }

    write(addr: number, val: number) {
        const hack = this.hack;
        if (hack) {
            for (const hackInfo of hack) {
                if (hackInfo.enable) {
                    for (const info of hackInfo.value) {
                        if (info.address === addr) {
                            val = info.value;
                        }
                    }
                }
            }
        }
        if (addr < 0x2000) {
            this.mem[addr & 0x7FF] = val;
        }
        else {
            this.nes.mmap.write(addr, val);
        }
    }

    requestIrq(type: IRQType) {
        if (this.irqRequested) {
            if (type == IRQType.NORMAL) {
                return;
            }
            ////System.out.println("too fast irqs. type="+type);
        }
        this.irqRequested = true;
        this.irqType = type;
    }

    push(value: number) {
        this.nes.mmap.write(this.REG_SP, value);
        this.REG_SP--;
        this.REG_SP = 0x0100 | (this.REG_SP & 0xFF);
    }

    stackWrap() {
        this.REG_SP = 0x0100 | (this.REG_SP & 0xFF);
    }

    pull() {
        this.REG_SP++;
        this.REG_SP = 0x0100 | (this.REG_SP & 0xFF);
        return this.nes.mmap.load(this.REG_SP);
    }

    pageCrossed(addr1: number, addr2: number) {
        return ((addr1 & 0xFF00) != (addr2 & 0xFF00));
    }

    haltCycles(cycles: number) {
        this.cyclesToHalt += cycles;
    }

    doNonMaskableInterrupt(status: number) {
        if ((this.nes.mmap.load(0x2000) & 128) != 0) { // Check whether VBlank Interrupts are enabled

            this.REG_PC_NEW++;
            this.push((this.REG_PC_NEW >> 8) & 0xFF);
            this.push(this.REG_PC_NEW & 0xFF);
            //this.F_INTERRUPT_NEW = 1;
            this.push(status);

            this.REG_PC_NEW = this.nes.mmap.load(0xFFFA) | (this.nes.mmap.load(0xFFFB) << 8);
            this.REG_PC_NEW--;
        }
    }

    doResetInterrupt() {
        this.REG_PC_NEW = this.nes.mmap.load(0xFFFC) | (this.nes.mmap.load(0xFFFD) << 8);
        this.REG_PC_NEW--;
    }

    doIrq(status: number) {
        this.REG_PC_NEW++;
        this.push((this.REG_PC_NEW >> 8) & 0xFF);
        this.push(this.REG_PC_NEW & 0xFF);
        this.push(status);
        this.F_INTERRUPT_NEW = 1;
        this.F_BRK_NEW = 0;

        this.REG_PC_NEW = this.nes.mmap.load(0xFFFE) | (this.nes.mmap.load(0xFFFF) << 8);
        this.REG_PC_NEW--;
    }

    getStatus() {
        return (this.F_CARRY)
            | (this.F_ZERO << 1)
            | (this.F_INTERRUPT << 2)
            | (this.F_DECIMAL << 3)
            | (this.F_BRK << 4)
            | (this.F_NOTUSED << 5)
            | (this.F_OVERFLOW << 6)
            | (this.F_SIGN << 7);
    }

    setStatus(st: number) {
        this.F_CARRY = (st) & 1;
        this.F_ZERO = (st >> 1) & 1;
        this.F_INTERRUPT = (st >> 2) & 1;
        this.F_DECIMAL = (st >> 3) & 1;
        this.F_BRK = (st >> 4) & 1;
        this.F_NOTUSED = (st >> 5) & 1;
        this.F_OVERFLOW = (st >> 6) & 1;
        this.F_SIGN = (st >> 7) & 1;
    }
}